/*
 * This file is part of Dependency-Track.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 * Copyright (c) Steve Springett. All Rights Reserved.
 */
package org.dependencytrack.model;

import alpine.json.TrimmedStringDeserializer;
import alpine.validation.RegexSequence;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.dependencytrack.util.VulnerabilityUtil;
import javax.jdo.annotations.Column;
import javax.jdo.annotations.Extension;
import javax.jdo.annotations.FetchGroup;
import javax.jdo.annotations.FetchGroups;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.Index;
import javax.jdo.annotations.Order;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import javax.jdo.annotations.Unique;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.UUID;

/**
 * Model for tracking vulnerabilities.
 *
 * @author Steve Springett
 * @since 3.0.0
 */
@PersistenceCapable
@FetchGroups({
        @FetchGroup(name = "COMPONENTS", members = {
                @Persistent(name = "components")
        })
})
@JsonInclude(JsonInclude.Include.NON_NULL)
@Unique(members = {"vulnId", "source"})
public class Vulnerability implements Serializable {

    private static final long serialVersionUID = -3002699553847728904L;

    /**
     * Defines the JDO fetch groups for this class.
     */
    public enum FetchGroup {
        COMPONENTS
    }

    /**
     * Defines the sources of vulnerability data supported by Dependency-Track.
     */
    public enum Source {
        NVD,      // National Vulnerability Database
        NPM,      // NPM Public Advisories
        VULNDB,   // VulnDB from Risk Based Security
        OSSINDEX, // Sonatype OSS Index
        RETIREJS, // Retire.js
        INTERNAL  // Internally-managed (and manually entered) vulnerability
    }

    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE)
    @JsonIgnore
    private long id;

    @Persistent
    @Column(name = "VULNID", allowsNull = "false")
    @Index(name = "VULNERABILITY_VULNID_IDX")
    @NotBlank
    @Size(min = 1, max = 255)
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The vulnerability ID may only contain printable characters")
    private String vulnId;

    @Persistent
    @Column(name = "SOURCE", allowsNull = "false")
    @NotBlank
    @Size(min = 1, max = 255)
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The source may only contain printable characters")
    private String source;

    @Persistent
    @Column(name = "FRIENDLYVULNID")
    @NotBlank
    @Size(min = 1, max = 255)
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The friendly vulnerability ID may only contain printable characters")
    private String friendlyVulnId;

    @Persistent
    @Column(name = "TITLE")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The title may only contain printable characters")
    private String title;

    @Persistent
    @Column(name = "SUBTITLE")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The subtitle may only contain printable characters")
    private String subTitle;

    @Persistent
    @Column(name = "DESCRIPTION", jdbcType = "CLOB")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The description may only contain printable characters")
    private String description;

    @Persistent
    @Column(name = "RECOMMENDATION", jdbcType = "CLOB")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The recommendation may only contain printable characters")
    private String recommendation;

    @Persistent
    @Column(name = "REFERENCES", jdbcType = "CLOB")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The references may only contain printable characters")
    private String references;

    @Persistent
    @Column(name = "CREDITS", jdbcType = "CLOB")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The credits may only contain printable characters")
    private String credits;

    @Persistent
    @Column(name = "CREATED")
    @Index(name = "VULNERABILITY_CREATED_IDX")
    private Date created;

    @Persistent
    @Column(name = "PUBLISHED")
    @Index(name = "VULNERABILITY_PUBLISHED_IDX")
    private Date published;

    @Persistent
    @Column(name = "UPDATED")
    @Index(name = "VULNERABILITY_UPDATED_IDX")
    private Date updated;

    @Persistent(defaultFetchGroup = "true")
    @Column(name = "CWE")
    @Index(name = "VULNERABILITY_CWE_IDX")
    private Cwe cwe;

    @Persistent
    @Column(name = "CVSSV2BASESCORE", scale = 1)
    private BigDecimal cvssV2BaseScore;

    @Persistent
    @Column(name = "CVSSV2IMPACTSCORE", scale = 1)
    private BigDecimal cvssV2ImpactSubScore;

    @Persistent
    @Column(name = "CVSSV2EXPLOITSCORE", scale = 1)
    private BigDecimal cvssV2ExploitabilitySubScore;

    @Persistent
    @Column(name = "CVSSV2VECTOR")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The CVSSv2 Vector may only contain printable characters")
    private String cvssV2Vector;

    @Persistent
    @Column(name = "CVSSV3BASESCORE", scale = 1)
    private BigDecimal cvssV3BaseScore;

    @Persistent
    @Column(name = "CVSSV3IMPACTSCORE", scale = 1)
    private BigDecimal cvssV3ImpactSubScore;

    @Persistent
    @Column(name = "CVSSV3EXPLOITSCORE", scale = 1)
    private BigDecimal cvssV3ExploitabilitySubScore;

    @Persistent
    @Column(name = "CVSSV3VECTOR")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The CVSSv3 Vector may only contain printable characters")
    private String cvssV3Vector;

    @Persistent
    @Column(name = "SEVERITY")
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The severity may only contain printable characters")
    private Severity severity;

    @Persistent
    @Column(name = "VULNERABLEVERSIONS")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The vulnerable versions may only contain printable characters")
    private String vulnerableVersions;

    @Persistent
    @Column(name = "PATCHEDVERSIONS")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The patched versions may only contain printable characters")
    private String patchedVersions;

    @Persistent
    @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC"))
    private List<VulnerableSoftware> vulnerableSoftware;

    @Persistent(mappedBy = "vulnerabilities")
    @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC"))
    private List<Component> components;

    @Persistent(mappedBy = "vulnerabilities")
    @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC"))
    private List<ServiceComponent> serviceComponents;

    @Persistent(customValueStrategy = "uuid")
    @Unique(name = "VULNERABILITY_UUID_IDX")
    @Column(name = "UUID", jdbcType = "VARCHAR", length = 36, allowsNull = "false")
    @NotNull
    private UUID uuid;

    private transient int affectedProjectCount;

    private transient FindingAttribution findingAttribution;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    /**
     * Returns the value of the severity field (if specified), otherwise, will
     * return the severity based on the numerical CVSS score. CVSSv2 and CVSSv3 have
     * slightly different ranges with CVSSv3 introducing critical severity whereas
     * CVSSv2 only has high, medium, and low.
     *
     * This method properly accounts for vulnerabilities that may have only a CVSSv2
     * score. If both scores are available, it will return the CVSSv3 severity.
     * @return the severity of the vulnerability
     * @see VulnerabilityUtil#getSeverity(BigDecimal, BigDecimal)
     */
    public Severity getSeverity() {
        return (this.severity != null) ? severity : VulnerabilityUtil.getSeverity(cvssV2BaseScore, cvssV3BaseScore);
    }

    /**
     * Sets the severity. This should only be set if CVSSv2 or CVSSv3 scores
     * are not used.
     * @param severity the severity of the vulnerability
     */
    public void setSeverity(Severity severity) {
        cvssV2BaseScore = null;
        cvssV2ImpactSubScore = null;
        cvssV2ExploitabilitySubScore = null;
        cvssV2Vector = null;
        cvssV3BaseScore = null;
        cvssV3ImpactSubScore = null;
        cvssV3ExploitabilitySubScore = null;
        cvssV3Vector = null;
        this.severity = severity;
    }

    public String getVulnId() {
        return vulnId;
    }

    public void setVulnId(String vulnId) {
        this.vulnId = vulnId;
    }

    public String getFriendlyVulnId() {
        return friendlyVulnId;
    }

    public void setFriendlyVulnId(String friendlyVulnId) {
        this.friendlyVulnId = friendlyVulnId;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getRecommendation() {
        return recommendation;
    }

    public void setRecommendation(String recommendation) {
        this.recommendation = recommendation;
    }

    public String getReferences() {
        return references;
    }

    public void setReferences(String references) {
        this.references = references;
    }

    public String getCredits() {
        return credits;
    }

    public void setCredits(String credits) {
        this.credits = credits;
    }

    public Date getCreated() {
        return created;
    }

    public void setCreated(Date created) {
        this.created = created;
    }

    public Date getPublished() {
        return published;
    }

    public void setPublished(Date published) {
        this.published = published;
    }

    public Date getUpdated() {
        return updated;
    }

    public void setUpdated(Date updated) {
        this.updated = updated;
    }

    public String getVulnerableVersions() {
        return vulnerableVersions;
    }

    public void setVulnerableVersions(String vulnerableVersions) {
        this.vulnerableVersions = vulnerableVersions;
    }

    public String getPatchedVersions() {
        return patchedVersions;
    }

    public void setPatchedVersions(String patchedVersions) {
        this.patchedVersions = patchedVersions;
    }

    public String getSource() {
        return source;
    }

    public void setSource(String source) {
        this.source = source;
    }

    public void setSource(Source source) {
        this.source = source.name();
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getSubTitle() {
        return subTitle;
    }

    public void setSubTitle(String subTitle) {
        this.subTitle = subTitle;
    }

    public Cwe getCwe() {
        return cwe;
    }

    public void setCwe(Cwe cwe) {
        this.cwe = cwe;
    }

    public BigDecimal getCvssV2BaseScore() {
        return cvssV2BaseScore;
    }

    public void setCvssV2BaseScore(BigDecimal cvssV2BaseScore) {
        this.cvssV2BaseScore = cvssV2BaseScore;
    }

    public BigDecimal getCvssV2ImpactSubScore() {
        return cvssV2ImpactSubScore;
    }

    public void setCvssV2ImpactSubScore(BigDecimal cvssV2ImpactSubScore) {
        this.cvssV2ImpactSubScore = cvssV2ImpactSubScore;
    }

    public BigDecimal getCvssV2ExploitabilitySubScore() {
        return cvssV2ExploitabilitySubScore;
    }

    public void setCvssV2ExploitabilitySubScore(BigDecimal cvssV2ExploitabilitySubScore) {
        this.cvssV2ExploitabilitySubScore = cvssV2ExploitabilitySubScore;
    }

    public String getCvssV2Vector() {
        return cvssV2Vector;
    }

    public void setCvssV2Vector(String cvssV2Vector) {
        this.cvssV2Vector = cvssV2Vector;
    }

    public BigDecimal getCvssV3BaseScore() {
        return cvssV3BaseScore;
    }

    public void setCvssV3BaseScore(BigDecimal cvssV3BaseScore) {
        this.cvssV3BaseScore = cvssV3BaseScore;
    }

    public BigDecimal getCvssV3ImpactSubScore() {
        return cvssV3ImpactSubScore;
    }

    public void setCvssV3ImpactSubScore(BigDecimal cvssV3ImpactSubScore) {
        this.cvssV3ImpactSubScore = cvssV3ImpactSubScore;
    }

    public BigDecimal getCvssV3ExploitabilitySubScore() {
        return cvssV3ExploitabilitySubScore;
    }

    public void setCvssV3ExploitabilitySubScore(BigDecimal cvssV3ExploitabilitySubScore) {
        this.cvssV3ExploitabilitySubScore = cvssV3ExploitabilitySubScore;
    }

    public String getCvssV3Vector() {
        return cvssV3Vector;
    }

    public void setCvssV3Vector(String cvssV3Vector) {
        this.cvssV3Vector = cvssV3Vector;
    }

    public List<VulnerableSoftware> getVulnerableSoftware() {
        return vulnerableSoftware;
    }

    public void setVulnerableSoftware(List<VulnerableSoftware> vulnerableSoftware) {
        this.vulnerableSoftware = vulnerableSoftware;
    }

    public List<Component> getComponents() {
        return components;
    }

    public void setComponents(List<Component> components) {
        this.components = components;
    }

    public List<ServiceComponent> getServiceComponents() {
        return serviceComponents;
    }

    public void setServiceComponents(List<ServiceComponent> serviceComponents) {
        this.serviceComponents = serviceComponents;
    }

    public UUID getUuid() {
        return uuid;
    }

    public void setUuid(UUID uuid) {
        this.uuid = uuid;
    }

    public int getAffectedProjectCount() {
        return affectedProjectCount;
    }

    public void setAffectedProjectCount(int affectedProjectCount) {
        this.affectedProjectCount = affectedProjectCount;
    }

    public FindingAttribution getFindingAttribution() {
        return findingAttribution;
    }

    public void setFindingAttribution(FindingAttribution findingAttribution) {
        this.findingAttribution = findingAttribution;
    }
}
